/**
 * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com).
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.jboot.utils;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.jfinal.core.JFinal;
import com.jfinal.kit.Base64Kit;
import com.jfinal.kit.HashKit;
import com.jfinal.kit.LogKit;
import com.jfinal.kit.PathKit;
import com.jfinal.upload.UploadFile;

public class FileUtil {
	private static Logger logger = LoggerFactory.getLogger(FileUtil.class);
	
    /**
     * 获取文件后缀
     *
     * @param fileName eg: jboot.jpg
     * @return suffix eg: .jpg
     */
    public static String getSuffix(String fileName) {
        if (fileName != null && fileName.contains(".")) {
            return fileName.substring(fileName.lastIndexOf("."));
        }
        return null;
    }


    public static String removePrefix(String src, String... prefixes) {
        if (src != null) {
            for (String prefix : prefixes) {
                if (src.startsWith(prefix)) {
                    return src.substring(prefix.length());
                }
            }
        }

        return src;
    }


    public static String removeSuffix(String src, String... suffixes) {
        if (src != null) {
            for (String suffix : suffixes) {
                if (src.endsWith(suffix)) {
                    return src.substring(0, suffix.length());
                }
            }
        }
        return src;
    }


    public static String removeRootPath(String src) {
        return removePrefix(src, PathKit.getWebRootPath());
    }


    public static String readString(File file) {
        return readString(file, JFinal.me().getConstants().getEncoding());
    }


    public static String readString(File file, String charsetName) {
        ByteArrayOutputStream baos = null;
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            for (int len = 0; (len = fis.read(buffer)) > 0; ) {
                baos.write(buffer, 0, len);
            }
            return baos.toString(charsetName);
        } catch (Exception e) {
            LogKit.error(e.toString(), e);
        } finally {
            close(fis, baos);
        }
        return null;
    }


    public static void writeString(File file, String content) {
        writeString(file, content, JFinal.me().getConstants().getEncoding());
    }


    public static void writeString(File file, String content, String charsetName) {
        writeString(file, content, charsetName, false);
    }

    public static void writeString(File file, String content, String charsetName, boolean append) {
        FileOutputStream fos = null;
        try {
            ensuresParentExists(file);
            fos = new FileOutputStream(file, append);
            fos.write(content.getBytes(charsetName));
        } catch (Exception e) {
            LogKit.error(e.toString(), e);
        } finally {
            close(fos);
        }
    }

    public static void ensuresParentExists(File currentFile) throws IOException {
        if (!currentFile.getParentFile().exists()
                && !currentFile.getParentFile().mkdirs()) {
            throw new IOException("Can not mkdirs for file: " + currentFile.getParentFile());
        }
    }


    /**
     * 获取文件的 md5
     *
     * @param file
     * @return
     */
    public static String getFileMD5(File file) {
        return getFileMD5(file, false);
    }


    /**
     * 获取文件 md5 的 base64 编码
     *
     * @param file
     * @return
     */
    public static String getFileMd5Base64(File file) {
        return getFileMD5(file, true);
    }


    private static String getFileMD5(File file, boolean withBase64) {
        try (FileInputStream fiStream = new FileInputStream(file)) {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            byte[] buffer = new byte[8192];
            int length;
            while ((length = fiStream.read(buffer)) != -1) {
                digest.update(buffer, 0, length);
            }
            return withBase64 ? Base64Kit.encode(digest.digest()) : HashKit.toHex(digest.digest());
        } catch (Exception e) {
            LogKit.error(e.toString(), e);
        }
        return null;
    }


    public static void close(Closeable... closeable) {
        QuietlyUtil.closeQuietly(closeable);
    }


    public static void unzip(String zipFilePath) throws IOException {
        String targetPath = zipFilePath.substring(0, zipFilePath.lastIndexOf("."));
        unzip(zipFilePath, targetPath, true, StandardCharsets.UTF_8);
    }


    public static void unzip(String zipFilePath, String targetPath) throws IOException {
        unzip(zipFilePath, targetPath, true, StandardCharsets.UTF_8);
    }


    public static void unzip(String zipFilePath, String targetPath, boolean safeUnzip, Charset charset) throws IOException {
        targetPath = getCanonicalPath(new File(targetPath));
        ZipFile zipFile = new ZipFile(zipFilePath, charset);
        try {
            Enumeration<?> entryEnum = zipFile.entries();
            while (entryEnum.hasMoreElements()) {
                OutputStream os = null;
                InputStream is = null;
                try {
                    ZipEntry zipEntry = (ZipEntry) entryEnum.nextElement();
                    if (!zipEntry.isDirectory()) {
                        if (safeUnzip && isNotSafeFile(zipEntry.getName())) {
                            //Unsafe
                            continue;
                        }

                        File targetFile = new File(targetPath, zipEntry.getName());

                        ensuresParentExists(targetFile);

                        if (!targetFile.toPath().normalize().startsWith(targetPath)) {
                            throw new IOException("Bad zip entry");
                        }

                        os = new BufferedOutputStream(new FileOutputStream(targetFile));
                        is = zipFile.getInputStream(zipEntry);
                        byte[] buffer = new byte[4096];
                        int readLen = 0;
                        while ((readLen = is.read(buffer, 0, 4096)) > 0) {
                            os.write(buffer, 0, readLen);
                        }
                    }
                } finally {
                    close(is, os);
                }
            }
        } finally {
            close(zipFile);
        }
    }
    
//        //测试方法1
//        toZip("E:\\省市区.zip", "E:\\省市区", false);
//        //测试方法2
//        List<File> fileList = new ArrayList<>();
//        fileList.add(new File("E:\\省市区\\dic_road_sh.xlsx"));
//        fileList.add(new File("E:\\省市区\\dist_streets.csv"));
//        toZip("E:\\省市区_PART.zip", fileList );
    /**
     * 压缩成ZIP 方法1
     *
     * @param zipFileName       压缩文件夹路径
     * @param sourceFileName    要压缩的文件路径
     * @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构;
     *                         false:所有文件跑到压缩包根目录下(注意：不保留目录结构可能会出现同名文件,会压缩失败)
     * @throws RuntimeException 压缩失败会抛出运行时异常
     */
    public static Boolean toZip(String zipFileName, String sourceFileName, boolean KeepDirStructure) {
        Boolean result = true;
        long start = System.currentTimeMillis();//开始
        ZipOutputStream zos = null;
        try {
            FileOutputStream fileOutputStream = new FileOutputStream(zipFileName);
            zos = new ZipOutputStream(fileOutputStream);
            File sourceFile = new File(sourceFileName);
            compress(sourceFile, zos, sourceFile.getName(), KeepDirStructure);
            long end = System.currentTimeMillis();//结束
            System.out.println("压缩完成，耗时：" + (end - start) + " 毫秒");
        } catch (Exception e) {
            result = false;
            e.printStackTrace();
        } finally {
            if (zos != null) {
                try {
                    zos.close();
                } catch (IOException e) {
                    e.getStackTrace();
                }
            }
        }
        return result;
    }
 
    /**
     * 压缩成ZIP 方法2  一次性压缩多个文件
     *
     * @param srcFiles 需要压缩的文件列表
     * @param zipFileName 压缩文件输出
     * @throws RuntimeException 压缩失败会抛出运行时异常
     */
    public static void toZip(String zipFileName, List<File> srcFiles) throws Exception {
        long start = System.currentTimeMillis();
        ZipOutputStream zos = null;
        try {
            FileOutputStream fileOutputStream = new FileOutputStream(zipFileName);
            zos = new ZipOutputStream(fileOutputStream);
            for (File srcFile : srcFiles) {
                compress(srcFile, zos, srcFile.getName(), true);
            }
            long end = System.currentTimeMillis();
            System.out.println("压缩完成，耗时：" + (end - start) + " 毫秒");
        } catch (Exception e) {
            throw new RuntimeException("zip error from ZipUtils", e);
        } finally {
            if (zos != null) {
                try {
                    zos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 压缩成ZIP 方法3  一次性压缩文件夹
     *
     * @param srcDir 需要压缩的文件夹
     * @param zipFileName 压缩文件输出
     * @throws RuntimeException 压缩失败会抛出运行时异常
     */
    public static void toZip(String zipFileName, File srcDir) throws Exception {
        long start = System.currentTimeMillis();
        ZipOutputStream zos = null;
        try {
            FileOutputStream fileOutputStream = new FileOutputStream(zipFileName);
            zos = new ZipOutputStream(fileOutputStream);
            File[] srcFiles = srcDir.listFiles();
            for (File srcFile : srcFiles) {
                compress(srcFile, zos, srcFile.getName(), true);
            }
            long end = System.currentTimeMillis();
            System.out.println("压缩完成，耗时：" + (end - start) + " 毫秒");
        } catch (Exception e) {
            throw new RuntimeException("zip error from ZipUtils", e);
        } finally {
            if (zos != null) {
                try {
                    zos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
 
    /**
     * 递归压缩方法
     *
     * @param sourceFile       源文件
     * @param zos              zip输出流
     * @param name             压缩后的名称
     * @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构;
     *                         false:所有文件跑到压缩包根目录下(注意：不保留目录结构可能会出现同名文件,会压缩失败)
     * @throws Exception
     */
    public static void compress(File sourceFile, ZipOutputStream zos, String name,
                                boolean KeepDirStructure) throws Exception {
        final byte[] buf = new byte[1024];
        if (sourceFile.isFile()) {
            // 向zip输出流中添加一个zip实体，构造器中name为zip实体的文件的名字
            zos.putNextEntry(new ZipEntry(name));
            // copy文件到zip输出流中
            int len;
            FileInputStream in = new FileInputStream(sourceFile);
            while ((len = in.read(buf)) != -1) {
                zos.write(buf, 0, len);
            }
            // Complete the entry
            zos.closeEntry();
            in.close();
        } else {
            File[] listFiles = sourceFile.listFiles();
            if (listFiles == null || listFiles.length == 0) {
                // 需要保留原来的文件结构时,需要对空文件夹进行处理
                if (KeepDirStructure) {
                    // 空文件夹的处理
                    zos.putNextEntry(new ZipEntry(name + "/"));
                    // 没有文件，不需要文件的copy
                    zos.closeEntry();
                }
            } else {
                for (File file : listFiles) {
                    // 判断是否需要保留原来的文件结构
                    if (KeepDirStructure) {
                        // 注意：file.getName()前面需要带上父文件夹的名字加一斜杠,
                        // 不然最后压缩包中就不能保留原来的文件结构,即：所有文件都跑到压缩包根目录下了
                        compress(file, zos, name + "/" + file.getName(), KeepDirStructure);
                    } else {
                        compress(file, zos, file.getName(), KeepDirStructure);
                    }
                }
            }
        }
    }

	// 目录标识判断符
	private static final String PATCH = "/";
	// 缓冲区大小
	private static final int BUFFER = 2048;

	public static void compress(File srcFile, ZipOutputStream zipOutputStream, String basePath) throws IOException {
		if (srcFile.isDirectory()) {
			compressDir(srcFile, zipOutputStream, basePath);
		} else {
			compressFile(srcFile, zipOutputStream, basePath);
		}
	}

	private static void compressDir(File dir, ZipOutputStream zipOutputStream, String basePath) throws IOException {
		// 获取文件列表
		File[] files = dir.listFiles();

		if (files.length < 1) {
			ZipEntry zipEntry = new ZipEntry(basePath + dir.getName() + PATCH);

			try {
				zipOutputStream.putNextEntry(zipEntry);
			    zipOutputStream.closeEntry();
			}
			catch(ZipException e) {
				logger.warn(e.getMessage(), e);
			}
		}

		for (int i = 0, size = files.length; i < size; i++) {
			compress(files[i], zipOutputStream, basePath + dir.getName() + PATCH);
		}
	}

	private static void compressFile(File file, ZipOutputStream zipOutputStream, String dir) throws IOException {
		// 压缩文件
		ZipEntry zipEntry = new ZipEntry(dir + file.getName());
		try {
			zipOutputStream.putNextEntry(zipEntry);
		}
		catch(ZipException e) {
			logger.warn(e.getMessage(), e);
			return;
		}

		// 读取文件
		BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));

		int count = 0;
		byte data[] = new byte[BUFFER];
		while ((count = bis.read(data, 0, BUFFER)) != -1) {
			zipOutputStream.write(data, 0, count);
		}
		bis.close();
		zipOutputStream.closeEntry();
	}

	public static void toZip(String project, String zipFile, String[] paths) {
		ZipOutputStream zipOutputStream = null;
		try {
			zipOutputStream = new ZipOutputStream(new FileOutputStream(new File(zipFile)));
//			zipOutputStream.setEncoding(CHAR_SET);
			if (paths.length > 0) {
				if (project.equals("/")) {
					project = "";
				}
				
				if (project.equals("/")) {
					project = "";
				}
				if (project.length() > 0 && !project.endsWith(PATCH)) {
					project = project + PATCH;
				}
				
				for (String path : paths) {
					compress(new File(path), zipOutputStream, project);
				}
			}
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		}
		finally {
			if (zipOutputStream != null) {
				// 冲刷输出流
				try {
					zipOutputStream.flush();
				} catch (IOException e) {
				}
				// 关闭输出流
				try {
					zipOutputStream.close();
				} catch (IOException e) {
				}
			}
		}
	}

	public static void toZip(String project, String zipFile, List<File> paths) {
		ZipOutputStream zipOutputStream = null;
		try {
			zipOutputStream = new ZipOutputStream(new FileOutputStream(new File(zipFile)));
//			zipOutputStream.setEncoding(CHAR_SET);
			if (paths.size() > 0) {
				if (project.equals("/")) {
					project = "";
				}
				
				if (project.equals("/")) {
					project = "";
				}
				if (project.length() > 0 && !project.endsWith(PATCH)) {
					project = project + PATCH;
				}
				
				for (File path : paths) {
					compress(path, zipOutputStream, project);
				}
			}
			// 冲刷输出流
			zipOutputStream.flush();
			// 关闭输出流
			zipOutputStream.close();
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		}
		finally {
			if (zipOutputStream != null) {
				// 冲刷输出流
				try {
					zipOutputStream.flush();
				} catch (IOException e) {
				}
				// 关闭输出流
				try {
					zipOutputStream.close();
				} catch (IOException e) {
				}
			}
		}
	}

	public static void toZip(String zipEntryPath, String zipFile, File path) {
		ZipOutputStream zipOutputStream = null;
		try {
			zipOutputStream = new ZipOutputStream(new FileOutputStream(new File(zipFile)));
//			zipOutputStream.setEncoding(CHAR_SET);

			if (zipEntryPath.equals("/")) {
				zipEntryPath = "";
			}
			
			if (zipEntryPath.equals("/")) {
				zipEntryPath = "";
			}
			if (zipEntryPath.length() > 0 && !zipEntryPath.endsWith(PATCH)) {
				zipEntryPath = zipEntryPath + PATCH;
			}
			
			compress(path, zipOutputStream, zipEntryPath);

			// 冲刷输出流
			zipOutputStream.flush();
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		}
		finally {
			if (zipOutputStream != null) {
				// 关闭输出流
				try {
					zipOutputStream.close();
				} catch (IOException e) {
				}
			}
		}
	}

	public static void toZip(String zipEntryPath, File source, OutputStream fos) {
		ZipOutputStream zipOutputStream = null;
		try {
			zipOutputStream = new ZipOutputStream(fos);

			if (zipEntryPath.equals("/")) {
				zipEntryPath = "";
			}
			
			if (zipEntryPath.equals("/")) {
				zipEntryPath = "";
			}
			if (zipEntryPath.length() > 0 && !zipEntryPath.endsWith(PATCH)) {
				zipEntryPath = zipEntryPath + PATCH;
			}
			
			compress(source, zipOutputStream, zipEntryPath);

			// 冲刷输出流
			zipOutputStream.flush();
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		}
	}

    private static boolean isNotSafeFile(String name) {
        name = name.toLowerCase();
        return name.endsWith(".jsp") || name.endsWith(".jspx");
    }


    public static boolean isAbsolutePath(String path) {
        return StrUtil.isNotBlank(path) && (path.startsWith("/") || path.indexOf(":") > 0);
    }


    public static String getCanonicalPath(File file) {
        try {
            return file.getCanonicalPath();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void delete(File file) {
        if (file == null) {
            return;
        }

        if (!file.delete()) {
            LogKit.error("File {} can not deleted!", getCanonicalPath(file));
        }
    }

    public static void delete(UploadFile file) {
        if (file == null) {
            return;
        }

        delete(file.getFile());
    }


    public static void delete(List<UploadFile> files) {
        if (files == null) {
            return;
        }

        files.forEach(FileUtil::delete);
    }

	public static void main(String[] args) {
		toZip("GQAA-202204-091（A29-Y）", "D:/test.zip", new String[] {"", ""});
	}
}
